package com.heima.search.service.Impl;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.search.dtos.UserSearchDto;
import com.heima.model.user.pojos.ApUser;
import com.heima.search.service.ApUserSearchService;
import com.heima.search.service.ArticleSearchService;
import com.heima.utils.thread.AppThreadLocalUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;


@Service
@Slf4j
public class ArticleSearchServiceImpl implements ArticleSearchService {
    @Autowired
    private RestHighLevelClient restHighLevelClient;
    @Autowired
    private ApUserSearchService apUserSearchService;
    private static final ObjectMapper objectMapper = new ObjectMapper();
    private static final String ES_INDEX_NAME = "app_info_article";
    private static final String PUBLISH_TIME = "publishTime";
    private static final String FIELD_TO_HIGHLIGHT = "title";
    private static final String HIGHLIGHT_FIELD = "h_" + FIELD_TO_HIGHLIGHT;
    private static final String HIGHLIGHT_PRE_TAG = "<font style='color: red; font-size: inherit;'>";
    private static final String HIGHLIGHT_POST_TAG = "</font>";
    private static final String QUERY_FILED_TITLE = "title";
    private static final String QUERY_FILED_CONTENT = "content";

    /**
     * 执行 es 搜索
     *
     * @param dto 搜索参数
     * @return 包含搜索结果列表的响应对象
     * @throws IOException              当 es 查询或结果处理出现 IO 异常时抛出
     * @throws IllegalArgumentException 当参数校验失败时抛出
     */
    @Override
    public ResponseResult<List<Map<String, String>>> search(UserSearchDto dto)
            throws IOException {
        log.info("开始执行搜索，参数：{}", dto);
        try {
            // 1.检查参数
            validateSearchParams(dto);

            // 1.1 异步调用, 保存搜索记录
            ApUser user = AppThreadLocalUtil.getUser();
            if(user != null && dto.getFromIndex() == 0){
                apUserSearchService.insert(dto.getSearchWords(), user.getId());
            }

            // 2. 构建查询请求
            SearchRequest searchRequest = buildSearchRequest(dto);
            log.debug("构建的SearchRequest：{}", searchRequest);

            // 3. 执行查询
            SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            log.info("搜索完成，命中总数：{}", searchResponse.getHits().getTotalHits().value);

            // 4. 处理结果
            return ResponseResult.okResult(processSearchResults(searchResponse));
        } catch (Exception e) {
            log.error("搜索执行失败，参数：{}，错误：{}", dto, e.getMessage(), e);
            throw e;
        }
    }

    /**
     * 验证搜索参数
     *
     * @param dto 用户搜索参数
     * @throws IllegalArgumentException 当参数无效时抛出
     */
    private void validateSearchParams(UserSearchDto dto) {
        if (dto == null || StringUtils.isBlank(dto.getSearchWords())) {
            log.warn("参数校验失败，搜索词为空");
            throw new IllegalArgumentException("Invalid search parameters");
        }
    }

    /**
     * 构建 SearchRequest 对象
     *
     * @param dto 用户搜索参数
     * @return 配置好的 SearchRequest 对象
     */
    private SearchRequest buildSearchRequest(UserSearchDto dto) {
        SearchRequest searchRequest = new SearchRequest(ES_INDEX_NAME);
        searchRequest.source(buildSearchSourceBuilder(dto));
        return searchRequest;
    }

    /**
     * 构建 SearchSourceBuilder
     *
     * @param dto 用户搜索参数
     * @return 配置好的 SearchSourceBuilder 对象
     */
    private SearchSourceBuilder buildSearchSourceBuilder(UserSearchDto dto) {
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        // 1.构建布尔查询
        sourceBuilder.query(buildBoolQuery(dto));

        // 2.分页设置
        sourceBuilder.from(0);
        sourceBuilder.size(dto.getPageSize());
        log.debug("设置分页参数，pageSize：{}", dto.getPageSize());

        // 3.排序设置
        sourceBuilder.sort(PUBLISH_TIME, SortOrder.DESC);

        // 4.高亮设置
        sourceBuilder.highlighter(buildHighlightBuilder());

        return sourceBuilder;
    }

    /**
     * 构建布尔查询
     *
     * @param dto 用户搜索参数DTO
     * @return 配置好的BoolQueryBuilder对象
     */
    private BoolQueryBuilder buildBoolQuery(UserSearchDto dto) {
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

        // 1.关键字查询
        boolQuery.must(buildQueryStringQuery(dto.getSearchWords()));

        // 2.时间范围过滤
        if (dto.getMinBeHotTime() == null) {
            dto.setMinBeHotTime(new Date()); // 设置默认值，即当前时间
        }
        boolQuery.filter(buildRangeQuery(dto.getMinBeHotTime().getTime()));
        log.debug("设置时间范围过滤，minBehotTime：{}", dto.getMinBeHotTime());

        return boolQuery;
    }

    /**
     * 构建 QueryString 查询
     *
     * @param searchWords 搜索关键词
     * @return 配置好的QueryStringQueryBuilder对象
     */
    private QueryStringQueryBuilder buildQueryStringQuery(String searchWords) {
        log.debug("构建QueryString查询，关键词：{}，搜索字段：{}, {}",
                searchWords, QUERY_FILED_TITLE, QUERY_FILED_CONTENT);
        return QueryBuilders.queryStringQuery(searchWords)
                .field(QUERY_FILED_TITLE)
                .field(QUERY_FILED_CONTENT)
                .defaultOperator(Operator.OR);
    }

    /**
     * 构建范围查询
     *
     * @param maxDate 最大日期时间戳
     * @return 配置好的 RangeQueryBuilder 对象
     */
    private RangeQueryBuilder buildRangeQuery(long maxDate) {
        return QueryBuilders.rangeQuery(PUBLISH_TIME).lt(maxDate);
    }

    /**
     * 构建高亮设置
     *
     * @return 配置好的 HighlightBuilder 对象
     */
    private HighlightBuilder buildHighlightBuilder() {
        log.debug("设置高亮字段：{}", FIELD_TO_HIGHLIGHT);
        return new HighlightBuilder()
                .field(FIELD_TO_HIGHLIGHT)
                .preTags(HIGHLIGHT_PRE_TAG)
                .postTags(HIGHLIGHT_POST_TAG);
    }

    /**
     * 处理搜索结果
     *
     * @param response es SearchResponse
     * @return 处理后的结果列表
     */
    private List<Map<String, String>> processSearchResults(SearchResponse response) {
        log.debug("开始处理搜索结果，命中数：{}", response.getHits().getHits().length);
        return Arrays.stream(response.getHits().getHits())
                .map(hit -> {
                    try {
                        return convertHitToMap(hit);
                    } catch (IOException e) {
                        log.error("转换SearchHit失败，hitId：{}，错误：{}", hit.getId(), e.getMessage(), e);
                        throw new RuntimeException(e);
                    }
                })
                .collect(Collectors.toList());
    }

    /**
     * 将 SearchHit转换为 Map
     *
     * @param hit es 返回的SearchHit对象
     * @return 转换后的 Map
     * @throws IOException 当 JSON 解析失败时抛出
     */
    private Map<String, String> convertHitToMap(SearchHit hit) throws IOException {
        Map<String, String> map = objectMapper.readValue(
                hit.getSourceAsString(),
                new TypeReference<Map<String, String>>() {
                });

        // 处理高亮
        String highlightedTitle = extractHighlightedTitle(hit);
        map.put(HIGHLIGHT_FIELD,
                highlightedTitle != null ? highlightedTitle : map.get(FIELD_TO_HIGHLIGHT));
        return map;
    }

    /**
     * 提取高亮标题
     *
     * @param hit es 返回的 SearchHit 对象
     * @return 高亮后的标题字符串，无高亮时返回 null
     */
    private String extractHighlightedTitle(SearchHit hit) {
        if (hit.getHighlightFields() == null || hit.getHighlightFields().isEmpty()) {
            return null;
        }

        HighlightField highlightField = hit.getHighlightFields().get(FIELD_TO_HIGHLIGHT);
        if (highlightField == null) {
            return null;
        }

        return StringUtils.join(highlightField.getFragments());
    }

}
